第6回 Spring環境におけるトランザクション処理
よく訓練されたアップル信者、都元です。2年越しの復活、Spring Frameworkの話題です。ベルセ◯クかっつーの。正直2年も経ってると思っていませんでした。感覚的には半年くらい…そんなわけないか。…まぁ、なんつーか適当にやってますんで、適当にお付き合いください(汗
さて前回は「次回はトランザクションです」なんていう終わり方をしたので、約束は果たすぞ。2年経ってても次回は次回だ。
トランザクション
さて今回はトランザクションの話。あまり深いことを論じ始めると分厚い本が書けてしまう分野でもありますので、まず概略をお話し、そしてその中の一部に絞って話を進めます。
まず「トランザクション」という言葉については、WikipediaのトランザクションやACIDの項目をさらっと読んでおいてください。要するに、よくある例えが銀行振込処理で、「振込元の残高を減らす処理」と「振込先の残高を増やす処理」をする時、その結果が「両方とも成功する」か「両方とも失敗する」か、どちらかになり、間違っても「どちらか一方だけが成功」した状態で処理が終わらないこと(原子性=アトミック=分割できない)を保証するような話です。
グローバルトランザクション と ローカルトランザクション
上記の銀行振込の例では、イメージとしては1つのDBの中の世界で完結した話でした。しかしトランザクションというのは1つのDBの中でだけ起こるものではありません。例えば物理的に独立分離している2つのDBの間でトランザクション処理を行いたい場合もあります。
また、「DBへの書き込み」の話だけではなく、例えば「キューへのメッセージ送信」や「メールの送信」等、外部に対する影響を与えるような処理は(実際にどのように実現できるのかは置いといて)いずれもトランザクション処理の一要素になり得ます。
例えば、「DBからレコードを消す」ことと「キューへメッセージを送信する」ことをアトミックに処理したい、なんていうケースはそこそこ容易に想像がつくと思います。
このように、「DB」や「キュー」など、複数のリソースにまたがったトランザクション処理を「グローバルトランザクション」と呼ぶ一方、1つのリソース内で完結するようなトランザクション処理を「ローカルトランザクション」と呼びます。
グローバルトランザクションの話はかなりややこしいので、本稿ではこれ以上グローバルには触れません。話の中心はローカルトランザクションであり、しかもRDB上のローカルトランザクションに的を絞ります。
JDBCにおけるトランザクション管理
try { con = dataSource.getConnection(); con.setAutoCommit(false); ps1 = con.prepareStatement("UPDATE xxx SET yyy=? WHERE zzz = 'foo'"); ps2 = con.prepareStatement("UPDATE xxx SET yyy=? WHERE zzz = 'bar'"); ps1.setFloat(1, 90); ps1.executeUpdate(); ps2.setFloat(1, 110); ps2.executeUpdate(); con.commit(); } catch (Exception e) { con.rollback(); logger.error("rollbacked", e); } finally { con.setAutoCommit(true); }
こんなの見たことありませんか? connectionに対して、commit
やrollback
メソッドを呼ぶことにより、2つのUPDATE文をアトミックに束ねています。これはJDBCという仕様による、RDBのトランザクションを扱うためのAPIであって、他のトランザクションリソース(例えばキュー)に対するトランザクション処理には利用できません。
また、RDBを扱うAPIはJDBC以外にも様々(JDOやHibernate等)ありますが、それぞれやはりAPIは異なるため、JDBC以外におけるトランザクション処理には利用できません。
Springにおけるトランザクション管理 - PlatformTransactionManager
前述の通り、トランザクションは「DB」だけに対するものではなく、「キュー」等もトランザクション制御のためのAPI(メソッド等)を持っている場合があります。しかし、各種リソースがそれぞれ独自のAPIを定義しているため、一般的に統一的な操作方法がありませんでした。
SpringはこれらをPlatformTransactionManager
というインターフェイスに抽象化して統一し、同じ操作でトランザクションを制御できるようになりました。
PlatformTransactionManager txManager = new DataSourceTransactionManager(dataSource); DefaultTransactionDefinition td = new DefaultTransactionDefinition(); td.setName("SomeTxName"); td.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = txManager.getTransaction(td); try { // ... アトミックに束ねたい処理たち ... txManager.commit(status); } catch (Exception ex) { txManager.rollback(status); logger.error("rollbacked", e); }
こんな感じです。RDB(DataSource
)に対するトランザクション処理を担うPlatformTransactionManager
の実装クラスはDataSourceTransactionManager
です。今のところRDB以外のトランザクションリソースに触れる予定もなく、グローバルトランザクションにも触れないので、その他の実装クラスを利用することは当面ないと思ってください。
もう少し便利に - TransactionTemplate
上に挙げた例は、抽象化がなされているものの、JDBCのトランザクション制御と同じようなものでした。生JDBC記述に対してJdbcTemplate
という便利クラスがあったのと同様に、生トランザクション記述に対してTransactionTemplate
という便利クラスがあります。
TransactionTemplate template = new TransactionTemplate(txManager); template.setName("SomeTxName"); template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); template.execute(new TransactionCallback() { @Override public String doInTransaction(TransactionStatus status) { // ... アトミックに束ねたい処理たち ... return "result"; } });
このように、コールバックメソッド内に書いた処理がすべてアトミックに処理されるようになります。executeメソッド呼び出し部分の記述は、Java 8のLambda記法を使えばもっとさらっと書けますね。
// ... template.execute(status -> { // ... アトミックに束ねたい処理たち ... return "result"; });
宣言的トランザクション
さて、今まで見てきたトランザクション制御のコードは、非常に手続き的でした。「(ここからここまでをトランザクション処理とするために)コレを実行して、次にコレを実行して」というコードの記述方法です。
一方、宣言的なトランザクションとは「ここからここまでがトランザクション処理 である 」という記述です。具体的なアレしてコレして、という手続きを隠蔽したコードの記述方法であり、Springはこれをサポートしています。
@Transactional public void execute() { // ... アトミックに束ねたい処理たち ... }
ものすごいシンプルですね。@Transactional
アノテーションを付与したメソッドの入り口と出口までがトランザクション処理で、このメソッドが末尾まできちんと実行され、正常終了した場合はcommit、そうではなく例外によって終了した場合はrollbackします。このアノテーションの裏側の、隠蔽されて見えないところで、txManager.commit
等が呼ばれるのです。
やってみよう
前回既に、@Transactional
アノテーションは利用しています。
public class BerserkerApplication { // ... @Transactional public void execute() { // 成功するDB書き込み操作 userRepos.save(new User("torazuka", "$2a$10$fx33wHST4ecwp53MB5QvROQtIYwkdCU2O3XJK6LuCmm415dRncluC")); // からの失敗 throw new RuntimeException(); } }
例えばこのように、DBへの書き込み動作の後に例外でexecute
を抜けるようなコードを書いてみて、これを実行してみます。
この結果、新しいユーザがusers
テーブルに追加されない(ロールバックされている)ことを確認してみましょう。
サンプルプロジェクト berserker v6.0
このコードを、GitHubに上げておきました。ご興味のある方は、下記のように実行してみてください。
$ git clone https://github.com/classmethod-sandbox/berserker.git $ cd berserker $ git checkout 6.0 $ ./gradlew execute :compileJava :processResources UP-TO-DATE :classes :execute 2016/04/09 10:58:43.033 [main] INFO o.s.c.a.AnnotationConfigApplicationContext:578 - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@71be98f5: startup date [Sat Apr 09 10:58:43 JST 2016]; root of context hierarchy 2016/04/09 10:58:43.913 [main] INFO o.s.j.d.DriverManagerDataSource:133 - Loaded JDBC driver: com.mysql.jdbc.Driver 2016/04/09 10:58:44.716 [main] INFO o.s.c.a.AnnotationConfigApplicationContext:960 - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@71be98f5: startup date [Sat Apr 09 10:58:43 JST 2016]; root of context hierarchy Exception in thread "main" java.lang.RuntimeException at jp.classmethod.example.berserker.DataAccessSample.execute(DataAccessSample.java:54) at jp.classmethod.example.berserker.DataAccessSample$$FastClassBySpringCGLIB$$81ff9740.invoke() at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:281) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655) at jp.classmethod.example.berserker.DataAccessSample$$EnhancerBySpringCGLIB$$899a9a05.execute() at jp.classmethod.example.berserker.DataAccessSample.main(DataAccessSample.java:40) :execute FAILED FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':execute'. > Process 'command '/Library/Java/JavaVirtualMachines/jdk1.8.0_66.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1 * Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. BUILD FAILED Total time: 5.74 secs
例外終了しているので FAILED となっていますが、想定通りです。さて、DBの確認。
$ mysql -uroot -e "SELECT * FROM users" berserker +----------+--------------------------------------------------------------+ | username | password | +----------+--------------------------------------------------------------+ | miyamoto | $2a$10$vxq4n5VB4bsgUlBK9DXV9edhX911Qz/5iYqjLi/6qZPp8Xl7ZACKC | | yokota | $2a$10$nkvNPCb3Y1z/GSearD7s7OBdS9BoLBss3D4enbFQIgNJDvr0Xincm | +----------+--------------------------------------------------------------+
torazuka
の行は増えていません。
まとめ
色々なトランザクション処理記述方法をご紹介しましたが、「宣言的トランザクション」が本命です。ただ、宣言的な記述は所謂黒魔術であり、慣れないうちは処理が追いかけづらい、というトレードオフがあります。
しかし、トランザクションの管理コードはプロジェクト内のあちこちに登場して、コードの見通しを悪くしてしまうため、この黒魔術を受け入れつつ、宣言的なトランザクションとしてプログラミングをするケースが多いと思います。